﻿namespace DotNetCommon.DiagnosticReport
{
    using DotNetCommon.Extensions;
    using DotNetCommon;
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.IO;
    using System.Linq;
    using System.Net.NetworkInformation;
    using System.Reflection;
    using System.Runtime;
    using System.Runtime.InteropServices;
    using System.Runtime.Versioning;
    using System.Text;
    using System.Text.RegularExpressions;
    using System.Threading;

    /// <summary>
    /// 当前运行环境诊断类, 可检测操作系统、进程、网卡、CPU、硬盘灯运行情况
    /// </summary>
    /// <example>
    /// <para>
    /// 示例代码:
    /// </para>
    /// <code>
    /// DiagnosticReport.Generate().ToString();
    /// </code>
    /// <para>输出代码精简如下: </para>
    /// <code>
    /// /** 
    /// |Diagnostic Report generated at: 12-10-2020 14:58:48.608 (+08:00) in: 113.2014 milliseconds.
    /// |
    /// |System|.........................................................................................
    /// |
    /// |	. OS - Name         : Microsoft Windows 10.0.18362
    /// |	. OS - Type         : Windows(Desktop)
    /// |	. OS - 64Bit        : True
    /// |	. .NET Framework    : .NET Core 3.1.8
    /// |	. Machine Name      : BC1358
    /// |	. FQDN              : BC1358.
    /// |	. User              : BC1358\BC
    /// |	. CPU               : Intel(R) Core(TM) i3-9100 CPU @ 3.60GHz
    /// |	. CPU Core Count    : 4
    /// |	. Installed RAM     : 16GB
    /// |	. System Directory  : C:\windows\system32
    /// |	. Current Directory : E:\jackletter\DotNetCommon\bin\Debug\netcoreapp3.0
    /// |	. Runtime Directory : C:\Program Files\dotnet\shared\Microsoft.NETCore.App\3.1.8\
    /// |	. Uptime            : 00Day 06Hour 46Min 27Sec
    /// |
    /// |Process|.............................................................................................
    /// |
    /// |	. Id                  : 19684
    /// |	. Name                : testhost
    /// |	. Started             : 12-10-2020 14:58:48.023 (+08:00)
    /// |	. Loaded In           : 00:00:00.6154202
    /// |	. Interactive         : True
    /// |	. Optimized           : True
    /// |	. 64Bit Process       : True
    /// |	. Server GC           : False
    /// |	. Large Address Aware : True
    /// |	. WorkingSet          : 52MB
    /// |	. Threads             : 22
    /// |	. IO Threads          : 4 &lt;-> 1,000
    /// |	. Worker Threads      : 4 &lt;-> 32,767
    /// |	. File Version        : 15.0.0
    /// |	. Product Version     : 16.5.0
    /// |	. Language            : 语言中性
    /// |	. Copyright           : © Microsoft Corporation.All rights reserved.
    /// |	. Original File Name  : testhost.dll
    /// ............
    /// |
    /// |Drives|.........................................................................
    /// |
    /// |	 ---------------------------------------------------------------------------
    /// |	| Name | Type  | Format | Label   | Capacity(GB) | Free(GB) | Available(GB) |
    /// |	|------|-------|--------|---------|--------------|----------|---------------|
    /// |	| C:\  | Fixed | NTFS   | Windows | 100          | 25       | 25            |
    /// |	| D:\  | Fixed | NTFS   | 新加卷     | 188          | 103      | 103           |
    /// |	| E:\  | Fixed | NTFS   | 新加卷     | 188          | 170      | 170           |
    /// |	 ---------------------------------------------------------------------------
    /// |
    /// |Assemblies|.........................................................................................................................................................
    /// |
    /// |	001| Name      : System.Private.CoreLib, Version= 4.0.0.0, Culture= neutral, PublicKeyToken= 7cec85d7bea7798e
    /// |	   . GAC       : False
    /// |	   . 64Bit     : True
    /// |	   . Optimized : True
    /// |	   . Framework : .NET 2, 3 or 3.5
    /// |	   . Location  : C:\Program Files\dotnet\shared\Microsoft.NETCore.App\3.1.8\System.Private.CoreLib.dll
    /// |	   . CodeBase  : file:///C:/Program Files/dotnet/shared/Microsoft.NETCore.App/3.1.8/System.Private.CoreLib.dll
    /// 
    /// |Environment-Variables|................................................
    /// |
    /// |	001. ALLUSERSPROFILE                     : C:\ProgramData
    /// |	002. APPDATA                             : C:\Users\BC\AppData\Roaming
    /// |	003. CommonProgramFiles                  : C:\Program Files\Common Files
    /// |	004. CommonProgramFiles(x86)             : C:\Program Files (x86)\Common Files
    /// |	005. CommonProgramW6432                  : C:\Program Files\Common Files
    /// |	006. COMPUTERNAME                        : BC1358
    /// |	016. NUMBER_OF_PROCESSORS                : 4
    /// |	017. OneDrive                            : C:\Users\BC\OneDrive
    /// |	018. OnlineServices                      : Online Services
    /// ......
    /// |Networks|..............................................
    /// |	. Sangfor SSL VPN CS Support System VNIC
    /// |	  ---------------------------------------
    /// |	  | Name               : 以太网 2
    /// |	  | MAC                : 00:FF:4E:4C:7C:1C
    /// |	  | Type               : Ethernet
    /// |	  | Status             : Down
    /// |	  | Supports Multicast : True
    /// |	  | Is Receive Only    : False
    /// |	  | Speed              : 1,073 Mbit/s
    /// |	  | IP Addresses       : [169.254.200.43]
    /// |	
    /// |	. Realtek RTL8821CE 802.11ac PCIe Adapter
    /// |	  ----------------------------------------
    /// |	  | Name               : WLAN
    /// |	  | MAC                : 40:5B:D8:96:1F:71
    /// |	  | Type               : Wireless80211
    /// |	  | Status             : Down
    /// |	  | Supports Multicast : True
    /// |	  | Is Receive Only    : False
    /// |	  | Speed              : 0 Mbit/s
    /// |	  | IP Addresses       : [169.254.8.167]
    /// |	
    /// \
    /// */
    /// </code>
    /// </example>
    public class DiagnosticReport
    {
        private const char Pipe = '|';
        private const char Dot = '.';
        private const char Dash = '-';
        private const char Space = ' ';
        private const char Colon = ':';
        private static readonly string NewLine = Environment.NewLine;
        private static readonly string LinePrefix = Pipe.ToString() + "\t";

        private static readonly DiagnosticReportType[] ReportSections =
        {
            DiagnosticReportType.System,
            DiagnosticReportType.Process,
            DiagnosticReportType.Drives,
            DiagnosticReportType.Assemblies,
            DiagnosticReportType.EnvironmentVariables,
            DiagnosticReportType.Networks
        };

        private static readonly IDictionary<DiagnosticReportType, Action<StringBuilder, DiagnosticReport>>
            SectionToFormattingMap = new Dictionary<DiagnosticReportType, Action<StringBuilder, DiagnosticReport>>
            {
                [DiagnosticReportType.System] = AddSystem,
                [DiagnosticReportType.Process] = AddProcess,
                [DiagnosticReportType.Drives] = AddDrives,
                [DiagnosticReportType.Assemblies] = AddAssemblies,
                [DiagnosticReportType.EnvironmentVariables] = AddEnvironmentVariables,
                [DiagnosticReportType.Networks] = AddNetwork
            };

        private static readonly string[] SystemHeaders =
        {
            "OS - Name",
            "OS - Type",
            "OS - 64Bit",
            ".NET Framework",
            "Machine Name",
            "FQDN",
            "Installed RAM",
            "CPU",
            "CPU Core Count",
            "User",
            "System Directory",
            "Current Directory",
            "Runtime Directory",
            "Uptime"
        };

        private static readonly string[] ProcessHeaders =
        {
            "Id",
            "Name",
            "Started",
            "Loaded In",
            "Optimized",
            "64Bit Process",
            "Large Address Aware",
            "Module Name",
            "Module File Name",
            "Product Name",
            "Original File Name",
            "File Name",
            "File Version",
            "Product Version",
            "Language",
            "Copyright",
            "WorkingSet",
            "Interactive",
            "CommandLine",
            "Server GC",
            "Threads",
            "IO Threads",
            "Worker Threads"
        };

        private static readonly string[] DrivesHeaders =
        {
            "Name", "Type", "Format", "Label", "Capacity(GB)", "Free(GB)", "Available(GB)"
        };

        private static readonly string[] AssemblyHeaders =
        {
            "Name", "GAC", "64Bit", "Optimized", "Framework", "Location", "CodeBase"
        };

        private static readonly string[] NetworkHeaders =
        {
            "Host", "Domain", "DHCP Scope", "Node Type", "Is WINS Proxy", "Name", "MAC",
            "Type", "Status", "Is Receive Only", "Supports Multicast", "Speed", "IP Addresses"
        };

        /// <summary>
        /// Creates a new instance of the <see cref="DiagnosticReport"/>.
        /// </summary>
        private DiagnosticReport(DiagnosticReportType type)
        {
            var sw = Stopwatch.StartNew();

            Timestamp = DateTimeOffset.Now;
            Type = type;

            SystemDetails = GetSystemDetails(type);
            ProcessDetails = GetProcessDetails(type);
            DriveDetails = GetDriveDetails(type);
            Assemblies = GetAssemblies(type);
            EnvironmentVariables = GetEnvironmentVariables(type);
            NetworkingDetails = GetNetworkDetails(type);

            TimeTaken = sw.Elapsed;
        }

        /// <summary>
        /// 诊断报告生成的时间
        /// </summary>
        public DateTimeOffset Timestamp { get; }

        /// <summary>
        /// 诊断报告生成花费的时间
        /// </summary>
        public TimeSpan TimeTaken { get; }

        /// <summary>
        /// 诊断报告类型
        /// </summary>
        public DiagnosticReportType Type { get; }

        /// <summary>
        /// 操作系统信息报告 <c>System</c>.
        /// </summary>
        public SystemDetails SystemDetails { get; }

        /// <summary>
        /// 当前进程信息报告 <c>Process</c>.
        /// </summary>
        public ProcessDetails ProcessDetails { get; }

        /// <summary>
        /// 硬盘信息报告 <c>Drives</c>.
        /// </summary>
        public DriveDetails[] DriveDetails { get; }

        /// <summary>
        /// 程序集信息报告 <c>Assemblies</c>.
        /// </summary>
        public AssemblyDetails[] Assemblies { get; }

        /// <summary>
        /// 环境变量
        /// </summary>
        public IDictionary<string, string> EnvironmentVariables { get; }

        /// <summary>
        /// 网络信息报告 <c>Networks</c>.
        /// </summary>
        public NetworkDetails NetworkingDetails { get; }

        /// <summary>
        /// 根据指定的报告类型生成诊断报告 <see cref="DiagnosticReport"/>.
        /// </summary>
        public static DiagnosticReport Generate(DiagnosticReportType type = DiagnosticReportType.Full) => new DiagnosticReport(type);

        /// <summary>
        /// 生成格式化的诊断报告文字 <see cref="string"/>.
        /// </summary>
        public override string ToString()
        {
            try
            {
                return GenerateImpl(this);
            }
            catch (Exception e)
            {
                return $"Unable to generate the Diagnostic Report. Error:{NewLine}\t{e}";
            }
        }

        private static SystemDetails GetSystemDetails(DiagnosticReportType type)
        {
            if (!IsReportEnabled(type, DiagnosticReportType.System)) { return default; }

            return new SystemDetails
            {
                OSName = RuntimeInformation.OSDescription,
                OSType = GetOSPlatform(),
                Is64BitOS = Environment.Is64BitOperatingSystem,
                DotNetFrameworkVersion = GetDotNetFrameworkVersion(),
                MachineName = Environment.MachineName,
                FQDN = NetworkHelper.GetFQDN(),
                User = Environment.UserDomainName + "\\" + Environment.UserName,
                CPU = GetProcessorName(),
                CPUCoreCount = (uint)Environment.ProcessorCount,
                InstalledRAMInGigaBytes = GetInstalledMemoryInGigaBytes(),
                SystemDirectory = Environment.SystemDirectory,
                CurrentDirectory = Environment.CurrentDirectory,
                RuntimeDirectory = RuntimeEnvironment.GetRuntimeDirectory(),
                Uptime = GetUptime()
            };
        }

        private static ProcessDetails GetProcessDetails(DiagnosticReportType type)
        {
            if (!IsReportEnabled(type, DiagnosticReportType.Process)) { return default; }

            using var p = Process.GetCurrentProcess();

            var pVerInfo = p.MainModule.FileVersionInfo;
            var result = new ProcessDetails
            {
                PID = p.Id,
                Name = p.ProcessName,
                Started = p.StartTime,
                LoadedIn = ApplicationHelper.GetProcessStartupDuration(),
                IsInteractive = Environment.UserInteractive,
                IsOptimized = IsOptimized(),
                Is64Bit = Environment.Is64BitProcess,
                IsServerGC = GCSettings.IsServerGC,
                IsLargeAddressAware = ApplicationHelper.IsProcessLargeAddressAware(),
                WorkingSetInMegaBytes = UnitConverter.BytesToMegaBytes(Environment.WorkingSet),
                FileVersion = pVerInfo.FileVersion,
                ProductVersion = pVerInfo.ProductVersion,
                Language = pVerInfo.Language,
                Copyright = pVerInfo.LegalCopyright,
                OriginalFileName = pVerInfo.OriginalFilename,
                FileName = pVerInfo.FileName,
                ModuleName = p.MainModule.ModuleName,
                ModuleFileName = p.MainModule.FileName,
                ProductName = pVerInfo.ProductName,
                CommandLine = Environment.GetCommandLineArgs()
            };

            ThreadPool.GetMinThreads(out int minWrkrs, out int minComplWrkers);
            ThreadPool.GetMaxThreads(out int maxWrkrs, out int maxComplWrkers);
            result.ThreadPoolMinCompletionPortCount = (uint)minComplWrkers;
            result.ThreadPoolMaxCompletionPortCount = (uint)maxComplWrkers;
            result.ThreadPoolMinWorkerCount = (uint)minWrkrs;
            result.ThreadPoolMaxWorkerCount = (uint)maxWrkrs;

            result.ThreadCount = (uint)p.Threads.OfType<ProcessThread>().Count();
            return result;

            static string IsOptimized()
            {
                var executingAssembly = Assembly.GetEntryAssembly();
                return executingAssembly is null
                    ? "N/A - Assembly was called from Unmanaged code."
                    : executingAssembly.IsOptimized().ToString();
            }
        }

        private static IDictionary<string, string> GetEnvironmentVariables(DiagnosticReportType type)
        {
            if (!IsReportEnabled(type, DiagnosticReportType.EnvironmentVariables)) { return default; }

            return Environment.GetEnvironmentVariables()
                .Cast<DictionaryEntry>()
                .ToDictionary(kv => kv.Key.ToString(), kv => kv.Value.ToString());
        }

        private static DriveDetails[] GetDriveDetails(DiagnosticReportType type)
        {
            if (!IsReportEnabled(type, DiagnosticReportType.Drives)) { return default; }

            return DriveInfo.GetDrives()
                .Select(d =>
                {
                    var dashString = Dash.ToString();
                    string driveFormat = string.Empty, volumeLabel = string.Empty;
                    double capacity = 0, free = 0, available = 0;

                    if (d.IsReady)
                    {
                        // ReSharper disable once ConstantNullCoalescingCondition
                        driveFormat = d.DriveFormat ?? dashString;
                        volumeLabel = d.VolumeLabel ?? dashString;
                        capacity = UnitConverter.BytesToGigaBytes(d.TotalSize);
                        free = UnitConverter.BytesToGigaBytes(d.TotalFreeSpace);
                        available = UnitConverter.BytesToGigaBytes(d.AvailableFreeSpace);
                    }

                    return new DriveDetails
                    {
                        Name = d.Name,
                        Type = d.DriveType.ToString(),
                        Format = driveFormat,
                        Label = volumeLabel,
                        TotalCapacityInGigaBytes = capacity,
                        FreeCapacityInGigaBytes = free,
                        AvailableCapacityInGigaBytes = available
                    };
                })
                .ToArray();
        }

        private static AssemblyDetails[] GetAssemblies(DiagnosticReportType type)
        {
            if (!IsReportEnabled(type, DiagnosticReportType.Assemblies)) { return default; }

            return AppDomain.CurrentDomain.GetAssemblies()
                .Where(ass => !ass.IsDynamic)
                .OrderByDescending(o => o.GlobalAssemblyCache)
                .Select(ass => new AssemblyDetails
                {
                    Name = ass.FullName,
                    IsGAC = ass.GlobalAssemblyCache,
                    Is64Bit = !ass.Is32Bit(),
                    IsOptimized = ass.IsOptimized(),
                    Framework = ass.GetFrameworkVersion(),
                    Location = ass.Location,
                    CodeBase = new Uri(ass.CodeBase)
                })
                .ToArray();
        }

        private static NetworkDetails GetNetworkDetails(DiagnosticReportType type)
        {
            if (!IsReportEnabled(type, DiagnosticReportType.Networks)) { return default; }

            var globalProps = IPGlobalProperties.GetIPGlobalProperties();

            return new NetworkDetails
            {
                DHCPScope = ApplicationHelper.IsWindows ? globalProps.DhcpScopeName : string.Empty,
                Domain = globalProps.DomainName,
                Host = globalProps.HostName,
                IsWINSProxy = ApplicationHelper.IsWindows && globalProps.IsWinsProxy,
                NodeType = globalProps.NodeType.ToString(),
                InterfaceDetails = NetworkInterface.GetAllNetworkInterfaces()
                    .Select(nic =>
                    {
                        var macAsBytes = nic.GetPhysicalAddress().GetAddressBytes();
                        return new NetworkInterfaceDetails
                        {
                            Interface = nic,
                            Addresses = NetworkHelper.GetLocalIPAddresses(nic).Select(IPAddressDetails.From).ToArray(),
                            MAC = string.Join(":", macAsBytes.Select(b => b.ToString("X2")))
                        };
                    })
                    .ToArray()
            };
        }

        private static string GenerateImpl(DiagnosticReport report)
        {
            var builder = new StringBuilder();

            foreach (var section in ReportSections)
            {
                if (IsReportEnabled(report.Type, section))
                {
                    SectionToFormattingMap[section](builder, report);
                }
            }

            builder.Insert(0, $"/{NewLine}{Pipe}Diagnostic Report generated at: " +
                              $"{report.Timestamp:dd-MM-yyyy HH:mm:ss.fff (zzzz)} in: " +
                              $"{report.TimeTaken.TotalMilliseconds} milliseconds.{NewLine}")
                .Append('\\');

            return builder.ToString();
        }

        private static void AddSystem(StringBuilder builder, DiagnosticReport report)
        {
            var maxHeaderLength = SystemHeaders.Max(h => h.Length);
            var formatter = "{0,-" + (maxHeaderLength + 1) + "}";

            var sectionIndex = builder.Length;
            Format(SystemHeaders[0], report.SystemDetails.OSName);
            Format(SystemHeaders[1], report.SystemDetails.OSType);
            Format(SystemHeaders[2], report.SystemDetails.Is64BitOS);
            Format(SystemHeaders[3], report.SystemDetails.DotNetFrameworkVersion);
            Format(SystemHeaders[4], report.SystemDetails.MachineName);
            Format(SystemHeaders[5], report.SystemDetails.FQDN);
            Format(SystemHeaders[9], report.SystemDetails.User);
            Format(SystemHeaders[7], report.SystemDetails.CPU);
            Format(SystemHeaders[8], report.SystemDetails.CPUCoreCount);
            Format(SystemHeaders[6], report.SystemDetails.InstalledRAMInGigaBytes.ToString("N0") + "GB");
            Format(SystemHeaders[10], report.SystemDetails.SystemDirectory);
            Format(SystemHeaders[11], report.SystemDetails.CurrentDirectory);
            Format(SystemHeaders[12], report.SystemDetails.RuntimeDirectory);

            var upTimeStr = "-";
            if (report.SystemDetails.Uptime != TimeSpan.MinValue)
            {
                upTimeStr = report.SystemDetails.Uptime.ToString(@"dd\D\a\y\ hh\H\o\u\r\ mm\M\i\n\ ss\S\e\c");
            }

            Format(SystemHeaders[13], upTimeStr);

            var maxLineLength = GetMaximumLineLength(builder, sectionIndex);
            builder.Insert(sectionIndex, GetSeperator("System", maxLineLength));

            void Format(string key, object value)
            {
                builder.Append(LinePrefix)
                    .Append(Dot)
                    .Append(Space)
                    .AppendFormat(formatter, key)
                    .Append(Colon)
                    .Append(Space)
                    .AppendLine(value?.ToString());
            }
        }

        private static void AddProcess(StringBuilder builder, DiagnosticReport report)
        {
            var maxHeaderLength = ProcessHeaders.Max(h => h.Length);
            var formatter = "{0,-" + (maxHeaderLength + 1) + "}";

            var sectionIndex = builder.Length;
            Format(ProcessHeaders[0], report.ProcessDetails.PID);
            Format(ProcessHeaders[1], report.ProcessDetails.Name);
            Format(ProcessHeaders[2], report.ProcessDetails.Started.ToString("dd-MM-yyyy HH:mm:ss.fff (zzzz)"));
            Format(ProcessHeaders[3], report.ProcessDetails.LoadedIn);
            Format(ProcessHeaders[17], report.ProcessDetails.IsInteractive);
            Format(ProcessHeaders[4], report.ProcessDetails.IsOptimized);
            Format(ProcessHeaders[5], report.ProcessDetails.Is64Bit);
            Format(ProcessHeaders[19], report.ProcessDetails.IsServerGC);
            Format(ProcessHeaders[6], report.ProcessDetails.IsLargeAddressAware);
            Format(ProcessHeaders[16], report.ProcessDetails.WorkingSetInMegaBytes.ToString("N0") + "MB");
            Format(ProcessHeaders[20], report.ProcessDetails.ThreadCount.ToString("N0"));
            Format(ProcessHeaders[21], $"{report.ProcessDetails.ThreadPoolMinCompletionPortCount:N0} <-> {report.ProcessDetails.ThreadPoolMaxCompletionPortCount:N0}");
            Format(ProcessHeaders[22], $"{report.ProcessDetails.ThreadPoolMinWorkerCount:N0} <-> {report.ProcessDetails.ThreadPoolMaxWorkerCount:N0}");
            Format(ProcessHeaders[12], report.ProcessDetails.FileVersion);
            Format(ProcessHeaders[13], report.ProcessDetails.ProductVersion);
            Format(ProcessHeaders[14], report.ProcessDetails.Language);
            Format(ProcessHeaders[15], report.ProcessDetails.Copyright);
            Format(ProcessHeaders[10], report.ProcessDetails.OriginalFileName);
            Format(ProcessHeaders[11], report.ProcessDetails.FileName);
            Format(ProcessHeaders[7], report.ProcessDetails.ModuleName);
            Format(ProcessHeaders[8], report.ProcessDetails.ModuleFileName);
            Format(ProcessHeaders[9], report.ProcessDetails.ProductName);
            Format(ProcessHeaders[18], report.ProcessDetails.CommandLine[0]);

            var cmdArgs = report.ProcessDetails.CommandLine;
            for (var i = 1; i < cmdArgs.Length; i++)
            {
                builder.Append(LinePrefix)
                    .Append(Space).Append(Space)
                    .AppendFormat(formatter, string.Empty)
                    .Append(Space).Append(Space)
                    .AppendLine(cmdArgs[i]);
            }

            var maxLineLength = GetMaximumLineLength(builder, sectionIndex);
            builder.Insert(sectionIndex, GetSeperator("Process", maxLineLength));

            void Format(string key, object value)
            {
                builder.Append(LinePrefix)
                    .Append(Dot)
                    .Append(Space)
                    .AppendFormat(formatter, key)
                    .Append(Colon)
                    .Append(Space)
                    .AppendLine(value?.ToString());
            }
        }

        private static void AddDrives(StringBuilder builder, DiagnosticReport report)
        {
            var values = new List<string[]>();

            foreach (var d in report.DriveDetails)
            {
                var row = new string[7];
                row[0] = d.Name;
                row[1] = d.Type;
                row[2] = d.Format;
                row[3] = d.Label;
                row[4] = d.TotalCapacityInGigaBytes.ToString("N0");
                row[5] = d.FreeCapacityInGigaBytes.ToString("N0");
                row[6] = d.AvailableCapacityInGigaBytes.ToString("N0");
                values.Add(row);
            }

            WrapInTable(builder, DrivesHeaders, values);
        }

        private static void AddAssemblies(StringBuilder builder, DiagnosticReport report)
        {
            var sectionIndex = builder.Length;

            var maxHeaderLength = AssemblyHeaders.Max(h => h.Length);

            var nameFormatter = "{0}{1:D3}{2} {3,-" + (maxHeaderLength + 1) + "}: {4}{5}";
            var formatter = "{0,-" + (maxHeaderLength + 1) + "}";

            var assCounter = 1;
            report.Assemblies
                .OrderByDescending(a => a.IsGAC)
                .ForEach(ass =>
                {
                    builder.AppendFormat(nameFormatter, LinePrefix, assCounter, Pipe, AssemblyHeaders[0], ass.Name, NewLine);

                    Format(AssemblyHeaders[1], ass.IsGAC);
                    Format(AssemblyHeaders[2], ass.Is64Bit);
                    Format(AssemblyHeaders[3], ass.IsOptimized);
                    Format(AssemblyHeaders[4], ass.Framework);
                    Format(AssemblyHeaders[5], ass.Location);
                    Format(AssemblyHeaders[6], ass.CodeBase);

                    if (assCounter != report.Assemblies.Length)
                    {
                        builder.Append(LinePrefix).AppendLine();
                    }

                    assCounter++;
                });

            var maxLineLength = GetMaximumLineLength(builder, sectionIndex);
            builder.Insert(sectionIndex, GetSeperator("Assemblies", maxLineLength));

            void Format(string key, object value) =>
                builder.Append(LinePrefix)
                    .Append(Space).Append(Space).Append(Space)
                    .Append(Dot)
                    .Append(Space)
                    .AppendFormat(formatter, key)
                    .Append(Colon)
                    .Append(Space)
                    .AppendLine(value?.ToString());
        }

        private static void AddEnvironmentVariables(StringBuilder builder, DiagnosticReport report)
        {
            var envKeyVals = report.EnvironmentVariables.OrderBy(kv => kv.Key)
                .ToDictionary(kv => kv.Key, kv => kv.Value);

            var sectionIndex = builder.Length;

            var envVarCounter = 1;
            var maxKeyLength = envKeyVals.Keys.Max(key => key.Length);
            var format = "{0}{1:D3}{2} {3,-" + maxKeyLength + "} : {4}{5}";
            envKeyVals.ForEach(kv =>
            {
                builder.AppendFormat(format, LinePrefix, envVarCounter, Dot, kv.Key, kv.Value, NewLine);
                envVarCounter++;
            });

            var maxLineLength = GetMaximumLineLength(builder, sectionIndex);
            builder.Insert(sectionIndex, GetSeperator("Environment-Variables", maxLineLength));
        }

        private static void AddNetwork(StringBuilder builder, DiagnosticReport report)
        {
            var net = report.NetworkingDetails;

            var sectionIndex = builder.Length;

            builder.Append(LinePrefix)
                .Append(Dot).Append(Space)
                .AppendLine("Windows IP Configuration")
                .Append(LinePrefix)
                .Append(Space).Append(Space)
                .AppendLine("-------------------------");

            var maxKeyLength = NetworkHeaders.Max(h => h.Length);
            var format = "{0}  {1} {2,-" + maxKeyLength.ToString() + "} : {3}{4}";

            Format(NetworkHeaders[0], net.Host);
            Format(NetworkHeaders[1], net.Domain);
            Format(NetworkHeaders[2], net.DHCPScope);
            Format(NetworkHeaders[3], net.NodeType);
            Format(NetworkHeaders[4], net.IsWINSProxy);

            builder.Append(LinePrefix).AppendLine();

            foreach (var item in net.InterfaceDetails)
            {
                builder.Append(LinePrefix)
                    .Append(Dot).Append(Space)
                    .AppendLine(item.Interface.Description)
                    .Append(LinePrefix)
                    .Append(Space).Append(Space)
                    .AppendLine(new string('-', item.Interface.Description.Length + 1));

                Format(NetworkHeaders[5], item.Interface.Name);
                Format(NetworkHeaders[6], item.MAC);
                Format(NetworkHeaders[7], item.Interface.NetworkInterfaceType);
                Format(NetworkHeaders[8], item.Interface.OperationalStatus);
                Format(NetworkHeaders[10], item.Interface.SupportsMulticast);

                if (ApplicationHelper.IsWindows)
                {
                    Format(NetworkHeaders[9], item.Interface.IsReceiveOnly);
                    Format(NetworkHeaders[11], (item.Interface.Speed / 1000000).ToString("N0") + " Mbit/s");
                }

                if (item.Addresses.Any())
                {
                    var ipAddresses = "[" + string.Join(" | ", item.Addresses.Select(i => i.ToString())) + "]";
                    Format(NetworkHeaders[12], ipAddresses);
                }

                builder.Append(LinePrefix).AppendLine();
            }

            var maxLineLength = GetMaximumLineLength(builder, sectionIndex);
            builder.Insert(sectionIndex, GetSeperator("Networks", maxLineLength));

            void Format(string key, object value)
            {
                // ReSharper disable once RedundantToStringCall
                builder.AppendFormat(format, LinePrefix, Pipe.ToString(), key, value?.ToString(), NewLine);
            }
        }

        private static void WrapInTable(StringBuilder builder, IReadOnlyList<string> columnHeaders, IReadOnlyList<string[]> values)
        {
            foreach (var row in values)
            {
                if (row.Length != columnHeaders.Count)
                {
                    throw new InvalidDataException("There should be a corresponding data for every column header");
                }
            }

            // initialize cellLengths first based on length of the headers
            var cellLengths = new int[columnHeaders.Count];
            for (var i = 0; i < columnHeaders.Count; i++)
            {
                var headerLength = columnHeaders[i].Length;
                cellLengths[i] = headerLength;
            }

            foreach (var row in values)
            {
                for (var i = 0; i < columnHeaders.Count; i++)
                {
                    var cellVal = row[i];
                    if (cellVal.Length > cellLengths[i])
                    {
                        cellLengths[i] = cellVal.Length;
                    }
                }
            }

            for (var i = 0; i < cellLengths.Length; i++)
            {
                cellLengths[i] = cellLengths[i] + 2;
            }

            var headerBuilder = new StringBuilder();

            // insert headers
            headerBuilder.Append(LinePrefix);
            for (var i = 0; i < columnHeaders.Count; i++)
            {
                var headerVal = columnHeaders[i];
                var formatter = "{0} {1,-" + (cellLengths[i] - 2).ToString() + "} ";
                headerBuilder.AppendFormat(formatter, Pipe.ToString(), headerVal);
            }
            headerBuilder.Append(Pipe).AppendLine();

            // insert headers underline
            headerBuilder.Append(LinePrefix);
            for (var i = 0; i < columnHeaders.Count; i++)
            {
                headerBuilder.Append(Pipe).Append(new string(Dash, cellLengths[i]));
            }

            var maxLineLengthInHeader = GetMaximumLineLength(headerBuilder);
            var beginAndEnd = $"{LinePrefix} {new string(Dash, maxLineLengthInHeader - LinePrefix.Length - 2)}{NewLine}";
            headerBuilder.Insert(0, beginAndEnd);

            var beginPos = builder.Length;

            // insert row values
            builder.Append(Pipe).AppendLine();
            foreach (var row in values)
            {
                builder.Append(LinePrefix);
                for (var j = 0; j < row.Length; j++)
                {
                    var formatter = "{0} {1,-" + (cellLengths[j] - 2).ToString() + "} ";
                    builder.AppendFormat(formatter, Pipe.ToString(), row[j]);
                }
                builder.Append(Pipe).AppendLine();
            }

            builder.Insert(beginPos, headerBuilder.ToString());
            builder.Append(beginAndEnd);

            var maxLineLength = GetMaximumLineLength(builder, beginPos);
            builder.Insert(beginPos, GetSeperator("Drives", maxLineLength));
        }

        private static int GetMaximumLineLength(StringBuilder builder, int start = 0)
        {
            if (start >= builder.Length) { throw new IndexOutOfRangeException(); }

            int maxLength = 0, tmpLength = 0;
            var prevChar = '\0';

            for (var i = start; i < builder.Length; i++)
            {
                var currChar = builder[i];

                if (currChar == '\n')
                {
                    if (prevChar == '\r') { --tmpLength; }
                    if (maxLength < tmpLength) { maxLength = tmpLength; }
                    tmpLength = 0;
                }
                else { tmpLength++; }

                prevChar = currChar;
            }
            return maxLength;
        }

        private static bool IsReportEnabled(DiagnosticReportType requestedType, DiagnosticReportType section)
        {
            var indexOf = Array.IndexOf(ReportSections, section);
            return ((int)requestedType & (1 << indexOf)) != 0;
        }

        private static string GetProcessorName()
        {
            var result = "UNKNOWN";

            try
            {
#pragma warning disable CA1416 // 验证平台兼容性
                if (ApplicationHelper.IsWindows) { result = GetProcessorNameWindows(); }
#pragma warning restore CA1416 // 验证平台兼容性
                if (ApplicationHelper.IsLinux) { result = GetProcessorNameLinux(); }
                if (ApplicationHelper.IsOSX) { result = GetProcessorNameOSX(); }

                return Regex.Replace(result, @"\s+", " ");
            }
            catch (Exception) { return result; }
        }

        /// <summary>
        /// Returns the full CPU name using the registry. 
        /// See <see href="http://stackoverflow.com/questions/2376139/get-full-cpu-name-without-wmi"/>
        /// </summary>
        /// <returns>The CPU Name</returns>
#if NET5_0
        [SupportedOSPlatform("windows")]
#endif
        private static string cpuName = "";
        private static string GetProcessorNameWindows()
        {
            //直接使用命令行获取cpu名称
            //var key = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(@"HARDWARE\DESCRIPTION\System\CentralProcessor\0\");
            //return key?.GetValue("ProcessorNameString").ToString() ?? "Not Found";
            if (cpuName.IsNullOrEmptyOrWhiteSpace())
            {
                lock (typeof(DiagnosticReport))
                {
                    if (cpuName.IsNullOrEmptyOrWhiteSpace())
                    {
                        var result = CmdHelper.RunCmd("wmic cpu get name");
                        try
                        {
                            var lines = result.SplitAndTrim(Environment.NewLine.ToArray()).ToList();
                            var index = lines.FindIndex(i => i.EndsWith("wmic cpu get name"));
                            index = lines.FindIndex(index + 1, i => i == "Name");

                            var endIndex = lines.FindLastIndex(i => i.EndsWith("exit"));
                            var cpus = lines.Skip(index + 1).Take(endIndex - index).Where(i => i.IsNotNullOrEmptyOrWhiteSpace());
                            cpuName = cpus.FirstOrDefault();
                        }
                        catch { cpuName = "Get CPU's Name Failed!"; }
                    }
                }
            }
            return cpuName;
        }

        private static string GetProcessorNameLinux()
        {
            // ReSharper disable once InconsistentNaming
            const string CPUFile = "/proc/cpuinfo";
            var cpuLine = File.ReadLines(CPUFile)
                .FirstOrDefault(l => l.StartsWith("model name", StringComparison.InvariantCultureIgnoreCase));

            if (cpuLine is null) { return "<UNKNOWN>"; }

            const string Separator = ": ";
            var startIdx = cpuLine.IndexOf(Separator, StringComparison.Ordinal) + Separator.Length;
            return cpuLine.Substring(startIdx, cpuLine.Length - startIdx);
        }

        // ReSharper disable once InconsistentNaming
        private static string GetProcessorNameOSX() => AsBashCommand("sysctl -n machdep.cpu.brand_string").TrimEnd();

        private static long GetInstalledMemoryInGigaBytes()
        {
            try
            {
                if (ApplicationHelper.IsWindows) { return GetInstalledMemoryInGigaBytesWindows(); }
                if (ApplicationHelper.IsLinux) { return GetInstalledMemoryInGigaBytesLinux(); }
                if (ApplicationHelper.IsOSX) { return GetInstalledMemoryInGigaBytesOSX(); }

                return 0;
            }
            catch (Exception) { return 0; }
        }

        private static long GetInstalledMemoryInGigaBytesWindows()
        {
            GetPhysicallyInstalledSystemMemory(out var installedMemoryKb);
            return (long)UnitConverter.KiloBytesToMegaBytes(installedMemoryKb).MegaBytesToGigaBytes();
        }

        private static long GetInstalledMemoryInGigaBytesLinux()
        {
            const string MemFile = "/proc/meminfo";
            var memLine = File.ReadLines(MemFile)
                .FirstOrDefault(l => l.StartsWith("MemTotal:", StringComparison.InvariantCultureIgnoreCase));

            if (memLine is null) { return -1; }

            const string BeginSeparator = ":";
            const string EndSeparator = "kB";
            var startIdx = memLine.IndexOf(BeginSeparator, StringComparison.Ordinal) + BeginSeparator.Length;
            var endIdx = memLine.IndexOf(EndSeparator, StringComparison.Ordinal);
            var memStr = memLine.Substring(startIdx, endIdx - startIdx);
            return long.Parse(memStr) / 1000_000;
        }

        // ReSharper disable once InconsistentNaming
        private static long GetInstalledMemoryInGigaBytesOSX()
        {
            var memStr = AsBashCommand("sysctl -n hw.memsize");
            return long.Parse(memStr) / 1000_000_000;
        }

        private static TimeSpan GetUptime()
        {
            try
            {
                if (ApplicationHelper.IsWindows) { return GetUptimeWindows(); }
                if (ApplicationHelper.IsLinux) { return GetUptimeLinux(); }
                return TimeSpan.MinValue;
            }
            catch (Exception)
            {
                return TimeSpan.MinValue;
            }
        }

        private static TimeSpan GetUptimeWindows()
        {
            var start = Stopwatch.StartNew();
            return TimeSpan.FromMilliseconds(GetTickCount64()).Subtract(start.Elapsed);
        }

        private static TimeSpan GetUptimeLinux()
        {
            const string UptimeFile = "/proc/uptime";
            var upTimeStr = File.ReadAllText(UptimeFile);

            var stopIdx = upTimeStr.IndexOf(Space);
            var upTimeSecStr = upTimeStr.Substring(0, stopIdx);
            var upTimeSec = double.Parse(upTimeSecStr);
            return TimeSpan.FromSeconds(upTimeSec);
        }

        private static string GetSeperator(string title, int count) =>
            $"{Pipe.ToString()}{NewLine}{Pipe.ToString()}{title}{Pipe.ToString()}{new string(Dot, count - title.Length)}{NewLine}{Pipe.ToString()}{NewLine}";

        private static string AsBashCommand(string command)
        {
            var escapedArgs = command.Replace("\"", "\\\"");
            using var p = new Process
            {
                EnableRaisingEvents = true,
                StartInfo = new ProcessStartInfo
                {
                    FileName = "sh",
                    Arguments = $"-c \"{escapedArgs}\"",
                    RedirectStandardOutput = true,
                    RedirectStandardError = true,
                    UseShellExecute = false,
                    CreateNoWindow = true
                }
            };

            p.Start();
            var result = p.StandardOutput.ReadToEnd();
            p.WaitForExit();
            return result;
        }

        private static string GetOSPlatform()
        {
            if (ApplicationHelper.OSPlatform == OSPlatform.Windows)
            {
                return IsWindowsServer() ? "Windows (Server)" : "Windows (Desktop)";
            }

            if (ApplicationHelper.OSPlatform == OSPlatform.Linux) { return "Linux"; }
            if (ApplicationHelper.OSPlatform == OSPlatform.OSX) { return "OSX"; }

            return ApplicationHelper.OSPlatform.ToString();
        }

        private static bool IsWindowsServer() => IsOS(OS_ANYSERVER);

        private static string GetDotNetFrameworkVersion()
        {
#if NETFRAMEWORK
            return ".NET Framework " + GetVersionFull();

            // https://docs.microsoft.com/en-us/dotnet/framework/migration-guide/how-to-determine-which-versions-are-installed
            static Version GetVersionFull()
            {
                const string REG_KEY = @"SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full";
                using var ndpKey = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(REG_KEY);

                if (ndpKey != null)
                {
                    var value = (int)(ndpKey.GetValue("Release") ?? 0);
                    if (value >= 461808) { return new Version(4, 7, 2); }
                    if (value >= 461308) { return new Version(4, 7, 1); }
                    if (value >= 460798) { return new Version(4, 7, 0); }
                    if (value >= 394802) { return new Version(4, 6, 2); }
                    if (value >= 394254) { return new Version(4, 6, 1); }
                    if (value >= 393295) { return new Version(4, 6, 0); }
                    if (value >= 379893) { return new Version(4, 5, 2); }
                    if (value >= 378675) { return new Version(4, 5, 1); }
                    if (value >= 378389) { return new Version(4, 5, 0); }

                    throw new NotSupportedException("Unable to detect a DotNet framework version of 4.5 or later, framework key value found: " + value.ToString());
                }

                throw new NotSupportedException($"Unable to find a registry key under '{REG_KEY}'.");
            }
#else
            try
            {
                return ".NET Core " + GetVersionCore();
            }
            catch (NotSupportedException)
            {
                return RuntimeInformation.FrameworkDescription + " (Self Contained)";
            }

            Version GetVersionCore()
            {
                const string REGEX_PATTERN = @"Microsoft\.NETCore\.App[\\,/](?<version>\d+\.\d+.\d+(.\d+)?)$";

                var runtimePath = Path.GetDirectoryName(typeof(object).Assembly.Location);

                var match = Regex.Match(runtimePath, REGEX_PATTERN);

                if (!match.Success)
                {
                    throw new NotSupportedException("Unable to detect a DotNet Core version, runtime path found: " + runtimePath);
                }

                return new Version(match.Groups["version"].Value);
            }
#endif
        }
        /// <summary>
        /// <see href="https://msdn.microsoft.com/en-us/library/windows/desktop/cc300158(v=vs.85).aspx"/>
        /// </summary>
        [DllImport("kernel32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool GetPhysicallyInstalledSystemMemory(out long totalMemoryInKilobytes);

        [DllImport("kernel32")]
        private static extern ulong GetTickCount64();

        [DllImport("shlwapi.dll", SetLastError = true, EntryPoint = "#437")]
        private static extern bool IsOS(int os);
        // ReSharper disable once InconsistentNaming
        private const int OS_ANYSERVER = 29;
    }
}